using System;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Management.Automation;
using System.Collections.ObjectModel;
using System.Management.Automation.Runspaces;

namespace Fines.Powershell.PowershellMSBuildTask
{
    /// <summary>
    /// The actual task class.
    /// </summary>
    public class Powershell : Task, IPowershellTask
    {
        public Powershell()
        {
            this._host = new PowershellHost( this );
        }

        /// <summary>
        /// The actual script.
        /// </summary>
        public ITaskItem Script { get; set; }

        /// <summary>
        /// Or an external script file
        /// </summary>
        public ITaskItem ScriptFile { get; set; }

        /// <summary>
        /// The output from the script.
        /// </summary>
        [Output]
        public ITaskItem[] Output { get; set; }

        /// <summary>
        /// Taskitems for input
        /// </summary>
        public ITaskItem[] TaskItems { get; set; }

        /// <summary>
        /// Strings for input.
        /// </summary>
        public string[] Arguments { get; set; }

        /// <summary>
        /// The value of $VerbosePreference.
        /// </summary>
        public string VerbosePreference { get; set; }

        /// <summary>
        /// The value of $VerbosePreference.
        /// </summary>
        public string DebugPreference { get; set; }

        /// <summary>
        /// The value of $ErrorActionPreference.
        /// </summary>
        public string ErrorActionPreference { get; set; }

        /// <summary>
        /// The value of $ShouldProcessPreference.
        /// </summary>
        public string ShouldProcessPreference { get; set; }

        /// <summary>
        /// The value of $ProcessReturnPreference.
        /// </summary>
        public bool ProcessReturnPreference { get; set; }


        bool IPowershellTask.ShouldExit { get; set; }


        int IPowershellTask.ExitCode { get; set; }

        public override bool Execute()
        {
            try
            {
                Validate();

                // Create the runspace and stuff.
                Runspace runspace = RunspaceFactory.CreateRunspace( this._host );
                Pipeline pipeline = runspace.CreatePipeline();

            
                this.SetUpOutputStreams( pipeline );

                runspace.Open();

                // Set up global variables.
                this.FillVariables(runspace);

                // Add our script to the pipeline.
                Command command;
                if (FilenameSpecified)
                {
                    string path = EnsureRooted(this.ScriptFile.ItemSpec);

                    command = new Command(path, false);

                    // Use the Arguments property to set the script Parameters
                    FillScriptFileParameters(command);
                }
                else
                {
                    command = new Command(this.Script.ItemSpec.Trim(), true);

                    // Use the Arguments property to set variables in the script
                    this.FillScriptBlockParameters(runspace);
                }

                pipeline.Commands.Add( command );

                // Run the script
                Collection<PSObject> psOutput = pipeline.Invoke();

                // Did it complete ok?
                PipelineStateInfo info = pipeline.PipelineStateInfo;
                if ( info.State != PipelineState.Completed )
                {
                    this.Log.LogErrorFromException( info.Reason, false );
                    return false;
                }

                // return the output items as strings.
                if ( psOutput != null )
                {
                    PSObject[] outputObjects = new PSObject[psOutput.Count];
                    psOutput.CopyTo(outputObjects, 0);

                    this.Output = Array.ConvertAll<PSObject, ITaskItem>( outputObjects,
                                                                         psObject =>
                                                                         psObject != null
                                                                             ? new TaskItem(psObject.ToString())
                                                                             : new TaskItem(String.Empty));

                    return true;
                }
                return false;
            }
            catch (RuntimeException rex)
            {
                // Catch the runtimeExceptions because in the ErrorRecord you can find what went wrong
                // (and at what line, which helps a lot while debugging)
                ErrorRecord record = rex.ErrorRecord;
                this.Log.LogErrorFromException(record.Exception);
                this.Log.LogError(record.InvocationInfo.PositionMessage);
                throw;
            }
            catch ( Exception ex )
            {
                this.Log.LogErrorFromException( ex, false );
                throw;
            }

        }

        private void Validate()
        {
            if ((this.Script == null ||
                 String.IsNullOrEmpty(this.Script.ItemSpec)) &&
                (this.ScriptFile == null ||
                 String.IsNullOrEmpty(this.ScriptFile.ItemSpec)))
            {

                throw new InvalidOperationException(
                    "Either Script or ScriptFile must be specified.");

            }
        }

        private static string EnsureRooted(string path)
        {
            return Path.IsPathRooted(path) ? path : Path.GetFullPath(path);
        }

        private bool FilenameSpecified
        {
            get
            {
                if (this.Script != null && !string.IsNullOrEmpty(this.Script.ItemSpec))
                    return false; // If a script block was specified use it.
                if (!string.IsNullOrEmpty(this.ScriptFile.ItemSpec))
                    return true;
                return false;
            }
        }

        private void SetUpOutputStreams( Pipeline pipeline )
        {
            // Write stuff piped to error to WriteErrorLine.
            pipeline.Error.DataReady += delegate
            {
                for ( int i = 0; i < pipeline.Error.Count; i++ )
                {
                    object obj = pipeline.Error.Read();
                    this._host.UI.WriteErrorLine( obj.ToString() );
                }
            };
        }

        /// <summary>
        /// Fills up the global variables available to the script.
        /// </summary>
        /// <param name="runspace"></param>
        private void FillVariables( Runspace runspace )
        {
            runspace.SessionStateProxy.SetVariable( "TaskItems", this.TaskItems );
            SetVariableIfNotNull( runspace, "VerbosePreference", this.VerbosePreference );
            SetVariableIfNotNull( runspace, "DebugPreference", this.DebugPreference );
            SetVariableIfNotNull( runspace, "ErrorActionPreference", this.ErrorActionPreference );
            SetVariableIfNotNull( runspace, "ShouldProcessPreference", this.ShouldProcessPreference );
            SetVariableIfNotNull( runspace, "ProcessReturnPreference", this.ProcessReturnPreference );
            
        }

        /// <summary>
        /// Sets named arguments to the task on the form "var=value".
        /// </summary>
        /// <param name="runspace"></param>
        private void FillScriptBlockParameters(Runspace runspace)
        {
            ParseArguments((name, value) => runspace.SessionStateProxy.SetVariable(name, value));
        }

        private void FillScriptFileParameters(Command command)
        {
            ParseArguments(
                (name, value) =>
                command.Parameters.Add(new CommandParameter(name, value)));
        }

        private void ParseArguments(Action<string, string> continuation)
        {
            if ( this.Arguments != null )
            {
                foreach (string argument in this.Arguments)
                {
                    int splitIdx = argument.IndexOf('=');
                    if (splitIdx <= 0)
                    {
                        throw new ArgumentException(String.Format("Invalid syntax for argument: {0}\r\nUse \"varname=value\"", argument));
                    }

                    string name = argument.Substring(0, splitIdx);
                    string value = argument.Substring(splitIdx + 1);

                    continuation(name, value);
                }
            }
        }


        private static void SetVariableIfNotNull( Runspace runspace, string variable, object value )
        {
            if ( value != null )
            {
                runspace.SessionStateProxy.SetVariable( variable, value );
            }
        }

        private readonly PowershellHost _host;

        // don't want a dependency on System.Core
        private delegate void Action<T1, T2>(T1 t1, T2 t2);
    }
}
